前面花了不少篇幅講 FP,今天要拉回來講前端技術 - Component Testing (元件測試)。
說起元件測試,雖然我知道這東西好一陣子了,也嘗試使用過 React Testing Library,但實際寫專案就是會懶得寫,心裡總是想著 UI 就瀏覽器開著邊寫邊看就好啦、有 E2E 和 unit test,很穩了。直到最近被我們大老闆抓到沒寫,被念閒聊一波之後才重新拾起一個重要觀念
容易測試的元件才是好元件
cypress 一開始是 end-to-end (點對點) 測試工具,大約2021年開始釋出元件測試功能,經過兩年發展目前看起來已經越來越成熟好用。真正讓我決定使用 Cypress 是看完這篇評比 Cypress Component Testing vs React Testing Library - the complete comparison。看起來 Cypress 語法比較簡潔、效能不錯、介面友善,而且在有使用 Cypress 做為 e2e 工具的情況下,不需要額外安裝好幾套工具,也更方便統計測試覆蓋率。
安裝 cypress
npm i -D cypress
修改 package.json
"scripts": {
...
"cy:e2e": "cypress run --e2e",
"cy:component": "cypress run --component",
"cy:open": "cypress open"
},
執行 npm run cy:open
,選擇 component testing
這時候應該會看到它已經自動偵測到我們是 Next.js 專案,並且提供一些預設設定檔案,這邊都一律下一步
如果出現錯誤,只要修改 tsconfig
,把 "moduleResolution": "bundler"
改成 "moduleResolution": "node"
到最後看到瀏覽器選單就表示安裝成功囉 !
cypress 很貼心的提供了根據現有元件自動建立測試模板的功能。以我們 TDD 流程來說,只要先寫一個空的 Component,然後滑鼠點點點,測試模板就自動生成了。
外觀看起來一開始會有點怪怪的,這是因為我們還沒有做 css 設定
import '../../src/app/globals.css'
只要在 component.ts
進行以上設定即可看到套用好的外觀元件
程式碼請參考 D09/component-test
const Initializer = () => {
const setCourseName = useSetAtom(courseNameAtom)
useEffect(() => {
setCourseName(initialCourseName) // 3. 重置 atom
}, [])
return <></>
}
describe('<CourseName />', () => {
beforeEach(() => {
ReactDom.unmountComponentAtNode(getContainerEl()) // 1. 清空所有掛載的元件
cy.mount(<Initializer />) // 2. 掛載初始化元件
cy.mount(<CourseName />) // 4. 掛載受測元件
})
})
Atom 會把各種狀態儲存在 React 的 context 裡面,導致我們難以透過 stub 等方式去做一個假的 atom。為了解決這個問題,我們要藉由建立一個初始化元件,在每次元件被掛載上去的時候,觸發 atom 的初始化。
還記得 AAA 嗎? 這邊做的就是 Arange
it('should not show error message and have empty input in the beginning', () => {
// assert
cy.get('input').should('have.value', '')
cy.get('.error-message').should('not.exist')
})
it('should not show error message on blur when input is invalid', () => {
// assert
cy.get('input').type('He110 w0r1d')
cy.get('.error-message').should('not.exist')
})
it('should show error message on blur when input is invalid', () => {
// act
cy.get('input').clear().focus().blur()
// assert
cy.get('.error-message')
})
it('should show error message on change when input is invalid', () => {
// act
cy.get('input').type('1{backspace}')
// assert
cy.get('.error-message')
})
一開始肯定全部失敗,因為我們連 atom 都還沒有準備,明天我們再來看看該怎麼通過所有檢查